/* Copyright (c) 2006 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.mttaboros.health.authsub;

import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Simple class for parsing and generating Content-Type header values, per
 * RFC 2045 (MIME) and 2616 (HTTP 1.1).
 */
final class ContentType {

    private static String TOKEN = "[\\p{ASCII}&&[^\\p{Cntrl} ;/=\\[\\]\\(\\)\\<\\>\\@\\,\\:\\\"\\?\\=]]+";

    // Precisely matches a token
    private static Pattern TOKEN_PATTERN = Pattern.compile("^" + TOKEN + "$");

    // Matches a media type value
    private static Pattern TYPE_PATTERN = Pattern.compile("(" + TOKEN + ")" +         // type  (G1)
            "/" +                       // separator
            "(" + TOKEN + ")" +         // subtype (G2)
            "\\s*(.*)\\s*", Pattern.DOTALL);

    // Matches an attribute value
    private static Pattern ATTR_PATTERN = Pattern.compile("\\s*;\\s*" + "(" + TOKEN + ")" +       // attr name  (G1)
            "\\s*=\\s*" + "(?:" + "\"([^\"]*)\"" +        // value as quoted string (G3)
            "|" + "(" + TOKEN + ")?" +    // value as token (G2)
            ")");

    /**
     * Name of the attribute that contains the encoding character set for
     * the content type.
     *
     * @see #getCharset()
     */
    public static final String ATTR_CHARSET = "charset";

    /**
     * Special "*" character to match any type or subtype.
     */
    private static final String STAR = "*";

    /**
     * The UTF-8 charset encoding is used by default for all text and xml
     * based MIME types.
     */
    private static final String DEFAULT_CHARSET = ATTR_CHARSET + "=UTF-8";

    /**
     * A ContentType constant that describes the Atom feed/entry content type.
     */
    public static final ContentType ATOM = new ContentType("application/atom+xml;" + DEFAULT_CHARSET);

    /**
     * A ContentType constant that describes the Atom Service content type.
     */
    public static final ContentType ATOM_SERVICE = new ContentType("application/atomserv+xml;" + DEFAULT_CHARSET);

    /**
     * A ContentType constant that describes the RSS channel/item content type.
     */
    public static final ContentType RSS = new ContentType("application/rss+xml;" + DEFAULT_CHARSET);

    /**
     * A ContentType constant that describes the JSON content type.
     */
    public static final ContentType JSON = new ContentType("application/json;" + DEFAULT_CHARSET);

    /**
     * A ContentType constant that describes the Javascript content type.
     */
    public static final ContentType JAVASCRIPT = new ContentType("text/javascript;" + DEFAULT_CHARSET);

    /**
     * A ContentType constant that describes the generic text/xml content type.
     */
    public static final ContentType TEXT_XML = new ContentType("text/xml;" + DEFAULT_CHARSET);

    /**
     * A ContentType constant that describes the generic text/plain content type.
     */
    public static final ContentType TEXT_PLAIN = new ContentType("text/plain;" + DEFAULT_CHARSET);

    /**
     * A ContentType constant that describes the MIME multipart/related content
     * type.
     */
    public static final ContentType MULTIPART_RELATED = new ContentType("multipart/related");

    /**
     * Determines the best "Content-Type" header to use in a servlet response
     * based on the "Accept" header from a servlet request.
     *
     * @param acceptHeader       "Accept" header value from a servlet request (not
     *                           <code>null</code>)
     * @param actualContentTypes actual content types in descending order of
     *                           preference (non-empty, and each entry is of the
     *                           form "type/subtype" without the wildcard char
     *                           '*') or <code>null</code> if no "Accept" header
     *                           was specified
     * @return the best content type to use (or <code>null</code> on no match).
     */
    public static ContentType getBestContentType(String acceptHeader, List/*<ContentType>*/ actualContentTypes) {

        // If not accept header is specified, return the first actual type
        if (acceptHeader == null) {
            return (ContentType) actualContentTypes.get(0);
        }

        // iterate over all of the accepted content types to find the best match
        Float bestQ = new Float(0);
        ContentType bestContentType = null;
        String[] acceptedTypes = acceptHeader.split(",");
        for (int i = 0; i < acceptedTypes.length; i++) {
            String acceptedTypeString = acceptedTypes[i];

            // create the content type object
            ContentType acceptedContentType;
            try {
                acceptedContentType = new ContentType(acceptedTypeString.trim());
            } catch (IllegalArgumentException ex) {
                // ignore exception
                continue;
            }

            // parse the "q" value (default of 1)
            Float curQ = new Float(1);
            try {
                String qAttr = acceptedContentType.getAttribute("q");
                if (qAttr != null) {
                    Float qValue = Float.valueOf(qAttr);
                    if (qValue.floatValue() <= 0 || qValue.floatValue() > 1) {
                        continue;
                    }
                    curQ = qValue;
                }
            } catch (NumberFormatException ex) {
                // ignore exception
                continue;
            }

            // only check it if it's at least as good ("q") as the best one so far
            if (curQ.floatValue() < bestQ.floatValue()) {
                continue;
            }

            /* iterate over the actual content types in order to find the best match
                     to the current accepted content type */
            for (Iterator iterator = actualContentTypes.iterator(); iterator.hasNext();) {
                ContentType actualContentType = (ContentType) iterator.next();

                /* if the "q" value is the same as the current best, only check for
                            better content types */
                if (curQ.equals(bestQ) && bestContentType == actualContentType) {
                    break;
                }

                /* check if the accepted content type matches the current actual
                            content type */
                if (actualContentType.match(acceptedContentType)) {
                    bestContentType = actualContentType;
                    bestQ = curQ;
                    break;
                }
            }
        }

        // if found an acceptable content type, return the best one
        if (bestQ.floatValue() != 0) {
            return bestContentType;
        }

        // Return null if no match
        return null;
    }

    /**
     * Constructs a new instance with default media type
     */
    public ContentType() {
        this(null);
    }

    /**
     * Constructs a new instance from a content-type header value
     * parsing the MIME content type (RFC2045) format.  If the type
     * is {@code null}, then media type and charset will be
     * initialized to default values.
     *
     * @param typeHeader content type value in RFC2045 header format.
     */
    public ContentType(String typeHeader) {

        // If the type header is no provided, then use the HTTP defaults.
        if (typeHeader == null) {
            type = "application";
            subType = "octet-stream";
            attributes.put(ATTR_CHARSET, "iso-8859-1"); // http default
            return;
        }

        // Get type and subtype
        Matcher typeMatch = TYPE_PATTERN.matcher(typeHeader);
        if (!typeMatch.matches()) {
            throw new IllegalArgumentException("Invalid media type:" + typeHeader);
        }

        type = typeMatch.group(1).toLowerCase();
        subType = typeMatch.group(2).toLowerCase();
        if (typeMatch.groupCount() < 3) {
            return;
        }

        // Get attributes (if any)
        Matcher attrMatch = ATTR_PATTERN.matcher(typeMatch.group(3));
        while (attrMatch.find()) {

            String value = attrMatch.group(2);
            if (value == null) {
                value = attrMatch.group(3);
                if (value == null) {
                    value = "";
                }
            }

            attributes.put(attrMatch.group(1).toLowerCase(), value);
        }

        // Infer a default charset encoding if unspecified.
        if (!attributes.containsKey(ATTR_CHARSET)) {
            inferredCharset = true;
            if (subType.endsWith("xml")) {
                if (type.equals("application")) {
                    // BUGBUG: Actually have need to look at the raw stream here, but
                    // if client omitted the charset for "application/xml", they are
                    // ignoring the STRONGLY RECOMMEND language in RFC 3023, sec 3.2.
                    // I have little sympathy.
                    attributes.put(ATTR_CHARSET, "utf-8");    // best guess
                } else {
                    attributes.put(ATTR_CHARSET, "us-ascii"); // RFC3023, sec 3.1
                }
            } else if (subType.equals("json")) {
                attributes.put(ATTR_CHARSET, "utf-8");    // RFC4627, sec 3
            } else {
                attributes.put(ATTR_CHARSET, "iso-8859-1"); // http default
            }
        }
    }

    /**
     * {code True} if parsed input didn't contain charset encoding info
     */
    private boolean inferredCharset = false;

    private String type;

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    private String subType;

    public String getSubType() {
        return subType;
    }

    public void setSubType(String subType) {
        this.subType = subType;
    }

    /**
     * Returns the full media type
     */
    public String getMediaType() {
        StringBuffer sb = new StringBuffer();
        sb.append(type);
        sb.append("/");
        sb.append(subType);
        return sb.toString();
    }

    private HashMap/*<String, String>*/ attributes = new HashMap/*<String, String>*/();

    /**
     * Returns the additional attributes of the content type.
     */
    public HashMap/*<String, String>*/ getAttributes() {
        return attributes;
    }

    /**
     * Returns the additional attribute by name of the content type.
     *
     * @param name attribute name
     */
    public String getAttribute(String name) {
        return (String) attributes.get(name);
    }

    /*
        * Returns the charset attribute of the content type or null if the
        * attribute has not been set.
        */
    public String getCharset() {
        return (String) attributes.get(ATTR_CHARSET);
    }

    /**
     * Returns whether this content type is match by the content type found in the
     * "Accept" header field of an HTTP request.
     *
     * @param acceptedContentType content type found in the "Accept" header field
     *                            of an HTTP request
     */
    public boolean match(ContentType acceptedContentType) {
        String acceptedType = acceptedContentType.getType();
        String acceptedSubType = acceptedContentType.getSubType();
        return STAR.equals(acceptedType) || type.equals(acceptedType) && (STAR.equals(acceptedSubType) || subType.equals(acceptedSubType));
    }

    /**
     * Generates the Content-Type value
     */
    public String toString() {

        StringBuffer sb = new StringBuffer();
        sb.append(type);
        sb.append("/");
        sb.append(subType);
        for (Iterator iterator = attributes.keySet().iterator(); iterator.hasNext();) {
            String name = (String) iterator.next();

            // Don't include any inferred charset attribute in output.
            if (inferredCharset && ATTR_CHARSET.equals(name)) {
                continue;
            }
            sb.append(";");
            sb.append(name);
            sb.append("=");
            String value = (String) attributes.get(name);
            Matcher tokenMatcher = TOKEN_PATTERN.matcher(value);
            if (tokenMatcher.matches()) {
                sb.append(value);
            } else {
                sb.append("\"" + value + "\"");
            }
        }
        return sb.toString();
    }

    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        ContentType that = (ContentType) o;
        return type.equals(that.type) && subType.equals(that.subType) && attributes
                .equals(that.attributes);
    }

    public int hashCode() {
        return (type.hashCode() * 31 + subType.hashCode()) * 31 + attributes
                .hashCode();
    }
}
